多对多关联

多对多关系比一对多复杂,需要中间表来连接。学生和课程、文章和标签、用户和角色,都是典型的多对多关系。

基本定义

用户和角色的多对多:

type User struct {
    ID    uint
    Name  string
    Roles []Role `gorm:"many2many:user_roles;"`
}

type Role struct {
    ID    uint
    Name  string
    Users []User `gorm:"many2many:user_roles;"`
}

GORM 会自动创建 user_roles 中间表:

CREATE TABLE user_roles (
    user_id INT,
    role_id INT,
    PRIMARY KEY (user_id, role_id)
)

中间表命名

默认中间表名是两个表名的组合,按字母顺序排列。

自定义中间表名:

type User struct {
    ID    uint
    Name  string
    Roles []Role `gorm:"many2many:sys_user_role;"`
}

自定义外键

中间表的外键名:

type User struct {
    ID    uint
    Name  string
    Roles []Role `gorm:"many2many:user_roles;foreignKey:ID;joinForeignKey:UserID"`
}

type Role struct {
    ID    uint
    Name  string
    Users []User `gorm:"many2many:user_roles;foreignKey:ID;joinForeignKey:RoleID"`
}
  • foreignKey - 本表的外键字段
  • joinForeignKey - 中间表中指向本表的外键名
  • References - 关联表的外键字段
  • joinReferences - 中间表中指向关联表的外键名

创建关联

创建用户时分配角色:

user := User{
    Name: "张三",
    Roles: []Role{
        {Name: "admin"},
        {Name: "editor"},
    },
}
db.Create(&user)

用户、角色、中间表记录都会创建。

如果角色已存在:

var roles []Role
db.Where("name IN ?", []string{"admin", "editor"}).Find(&roles)

user := User{
    Name:  "张三",
    Roles: roles,
}
db.Create(&user)

查询关联

var user User
db.Preload("Roles").First(&user, 1)
fmt.Println(user.Roles)

嵌套预加载:

type Role struct {
    ID          uint
    Name        string
    Users       []User `gorm:"many2many:user_roles;"`
    Permissions []Permission `gorm:"many2many:role_permissions;"`
}

db.Preload("Roles.Permissions").First(&user, 1)

添加关联

给用户添加角色:

var user User
db.First(&user, 1)

var role Role
db.First(&role, 1)

db.Model(&user).Association("Roles").Append(&role)

批量添加:

var roles []Role
db.Where("name IN ?", []string{"viewer", "guest"}).Find(&roles)
db.Model(&user).Association("Roles").Append(roles)

替换关联

替换用户的所有角色:

var roles []Role
db.Where("name IN ?", []string{"admin", "super"}).Find(&roles)
db.Model(&user).Association("Roles").Replace(roles)

删除关联

删除用户的某个角色:

db.Model(&user).Association("Roles").Delete(&role)

只删除中间表记录,不删除角色本身。

清空关联

清空用户的所有角色:

db.Model(&user).Association("Roles").Clear()

统计关联

count := db.Model(&user).Association("Roles").Count()

中间表额外字段

中间表有时需要额外字段,比如创建时间:

type User struct {
    ID    uint
    Name  string
    Roles []Role `gorm:"many2many:user_roles;"`
}

type Role struct {
    ID    uint
    Name  string
    Users []User `gorm:"many2many:user_roles;"`
}

type UserRole struct {
    UserID    uint
    RoleID    uint
    CreatedAt time.Time
    CreatedBy uint
}

手动操作中间表:

userRole := UserRole{
    UserID:    1,
    RoleID:    2,
    CreatedAt: time.Now(),
    CreatedBy: 1,
}
db.Create(&userRole)

自引用多对多

用户关注用户:

type User struct {
    ID        uint
    Name      string
    Following []User `gorm:"many2many:user_follows;foreignKey:ID;joinForeignKey:FollowerID;References:ID;joinReferences:FollowingID"`
    Followers []User `gorm:"many2many:user_follows;foreignKey:ID;joinForeignKey:FollowingID;References:ID;joinReferences:FollowerID"`
}

查询用户的关注列表:

var user User
db.Preload("Following").First(&user, 1)

查询用户的粉丝:

db.Preload("Followers").First(&user, 1)

实际案例

文章与标签

type Article struct {
    ID      uint
    Title   string
    Tags    []Tag `gorm:"many2many:article_tags;"`
}

type Tag struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"many2many:article_tags;"`
}

var article Article
db.Preload("Tags").First(&article, 1)

学生与课程

type Student struct {
    ID      uint
    Name    string
    Courses []Course `gorm:"many2many:student_courses;"`
}

type Course struct {
    ID       uint
    Name     string
    Students []Student `gorm:"many2many:student_courses;"`
}

商品与分类

商品可以属于多个分类,分类下有多个商品:

type Product struct {
    ID         uint
    Name       string
    Categories []Category `gorm:"many2many:product_categories;"`
}

type Category struct {
    ID       uint
    Name     string
    Products []Product `gorm:"many2many:product_categories;"`
}

常见问题

中间表不存在

GORM 会自动创建中间表,但需要执行 AutoMigrate:

db.AutoMigrate(&User{}, &Role{})

关联重复添加

同一个角色添加两次,中间表会有两条记录吗?GORM 会去重,不会重复插入。

删除时中间表

删除用户或角色时,中间表记录不会自动删除。需要配置约束:

type User struct {
    ID    uint
    Roles []Role `gorm:"many2many:user_roles;constraint:OnDelete:CASCADE;"`
}

小结

多对多关系需要中间表,GORM 自动处理大部分细节。理解外键配置,掌握关联操作方法,能应对复杂业务场景。